From bf0f3a82cfd497ef1185b1c62472b2155e46105e Mon Sep 17 00:00:00 2001 From: Matthias Clasen Date: Tue, 13 Oct 2020 00:50:19 -0400 Subject: [PATCH] atspi: Make text change notification work Make text change notification work for editables, by connecting to the ::insert-text and ::delete-text signals on the wrapped GtkText widget, and for GtkTextView by connecting to the corresponding GtkTextBuffer signals. This code is more or less directly copied from GtkTextViewAccessible and GtkEntryAccessible in GTK 3. --- gtk/a11y/gtkatspicontext.c | 71 ++++++++ gtk/a11y/gtkatspitext.c | 303 +++++++++++++++++++++++++++++++-- gtk/a11y/gtkatspitextprivate.h | 15 ++ 3 files changed, 379 insertions(+), 10 deletions(-) diff --git a/gtk/a11y/gtkatspicontext.c b/gtk/a11y/gtkatspicontext.c index 92562e4a04..9a5f58fd47 100644 --- a/gtk/a11y/gtkatspicontext.c +++ b/gtk/a11y/gtkatspicontext.c @@ -39,6 +39,7 @@ #include "gtkdebug.h" #include "gtkeditable.h" +#include "gtkentryprivate.h" #include "gtkroot.h" #include "gtktextview.h" #include "gtkwindow.h" @@ -670,6 +671,38 @@ gtk_at_spi_context_unregister_object (GtkAtSpiContext *self) } } +static void +emit_text_changed (GtkAtSpiContext *self, + const char *kind, + int start, + int end, + const char *text) +{ + g_dbus_connection_emit_signal (self->connection, + NULL, + self->context_path, + "org.a11y.atspi.Event.Object", + "TextChanged", + g_variant_new ("(siiva{sv})", + kind, start, end, g_variant_new_string (text), NULL), + NULL); +} + +static void +emit_selection_changed (GtkAtSpiContext *self, + const char *kind, + int cursor_position) +{ + g_dbus_connection_emit_signal (self->connection, + NULL, + self->context_path, + "org.a11y.atspi.Event.Object", + "TextChanged", + g_variant_new ("(siiva{sv})", + kind, cursor_position, 0, g_variant_new_string (""), NULL), + NULL); +} + static void emit_state_changed (GtkAtSpiContext *self, const char *name, @@ -874,12 +907,45 @@ gtk_at_spi_context_state_change (GtkATContext *ctx, } } +static void +insert_text_cb (GtkEditable *editable, + char *new_text, + int new_text_length, + int *position, + GtkAtSpiContext *self) +{ + int length; + + if (new_text_length == 0) + return; + + length = g_utf8_strlen (new_text, new_text_length); + emit_text_changed (self, "insert", *position - length, length, new_text); +} + +static void +delete_text_cb (GtkEditable *editable, + int start, + int end, + GtkAtSpiContext *self) +{ + char *text; + + if (start == end) + return; + + text = gtk_editable_get_chars (editable, start, end); + emit_text_changed (self, "delete", start, end - start, text); +} + static void gtk_at_spi_context_dispose (GObject *gobject) { GtkAtSpiContext *self = GTK_AT_SPI_CONTEXT (gobject); + GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self)); gtk_at_spi_context_unregister_object (self); + gtk_atspi_disconnect_text_signals (GTK_WIDGET (accessible)); G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->dispose (gobject); } @@ -1002,6 +1068,11 @@ gtk_at_spi_context_constructed (GObject *gobject) g_free (base_path); g_free (uuid); + GtkAccessible *accessible = gtk_at_context_get_accessible (GTK_AT_CONTEXT (self)); + gtk_atspi_connect_text_signals (GTK_WIDGET (accessible), + emit_text_changed, + emit_selection_changed, + self); gtk_at_spi_context_register_object (self); G_OBJECT_CLASS (gtk_at_spi_context_parent_class)->constructed (gobject); diff --git a/gtk/a11y/gtkatspitext.c b/gtk/a11y/gtkatspitext.c index bf8bdb79c4..9a7b37b5a3 100644 --- a/gtk/a11y/gtkatspitext.c +++ b/gtk/a11y/gtkatspitext.c @@ -404,6 +404,20 @@ static const GDBusInterfaceVTable label_vtable = { NULL, }; +static GtkText * +gtk_editable_get_text_widget (GtkWidget *widget) +{ + if (GTK_IS_ENTRY (widget)) + return gtk_entry_get_text_widget (GTK_ENTRY (widget)); + else if (GTK_IS_SEARCH_ENTRY (widget)) + return gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget)); + else if (GTK_IS_PASSWORD_ENTRY (widget)) + return gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget)); + else if (GTK_IS_SPIN_BUTTON (widget)) + return gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget)); + + return NULL; +} static void entry_handle_method (GDBusConnection *connection, @@ -418,16 +432,7 @@ entry_handle_method (GDBusConnection *connection, GtkATContext *self = user_data; GtkAccessible *accessible = gtk_at_context_get_accessible (self); GtkWidget *widget = GTK_WIDGET (accessible); - GtkText *text_widget; - - if (GTK_IS_ENTRY (widget)) - text_widget = gtk_entry_get_text_widget (GTK_ENTRY (widget)); - else if (GTK_IS_SEARCH_ENTRY (widget)) - text_widget = gtk_search_entry_get_text_widget (GTK_SEARCH_ENTRY (widget)); - else if (GTK_IS_PASSWORD_ENTRY (widget)) - text_widget = gtk_password_entry_get_text_widget (GTK_PASSWORD_ENTRY (widget)); - else if (GTK_IS_SPIN_BUTTON (widget)) - text_widget = gtk_spin_button_get_text_widget (GTK_SPIN_BUTTON (widget)); + GtkText *text_widget = gtk_editable_get_text_widget (widget); if (g_strcmp0 (method_name, "GetCaretOffset") == 0) { @@ -1165,3 +1170,281 @@ gtk_atspi_get_text_vtable (GtkWidget *widget) return NULL; } + +typedef struct { + void (* text_changed) (gpointer data, + const char *kind, + int start, + int end, + const char *text); + void (* selection_changed) (gpointer data, + const char *kind, + int cursor_position); + + gpointer data; + GtkTextBuffer *buffer; + int cursor_position; + int selection_bound; +} TextChanged; + +static void +insert_text_cb (GtkEditable *editable, + char *new_text, + int new_text_length, + int *position, + TextChanged *changed) +{ + int length; + + if (new_text_length == 0) + return; + + length = g_utf8_strlen (new_text, new_text_length); + changed->text_changed (changed->data, "insert", *position - length, length, new_text); +} + +static void +delete_text_cb (GtkEditable *editable, + int start, + int end, + TextChanged *changed) +{ + char *text; + + if (start == end) + return; + + text = gtk_editable_get_chars (editable, start, end); + changed->text_changed (changed->data, "delete", start, end - start, text); + g_free (text); +} + +static void +update_selection (TextChanged *changed, + int cursor_position, + int selection_bound) +{ + gboolean caret_moved, bound_moved; + + caret_moved = cursor_position != changed->cursor_position; + bound_moved = selection_bound != changed->selection_bound; + + if (!caret_moved && !bound_moved) + return; + + changed->cursor_position = cursor_position; + changed->selection_bound = selection_bound; + + if (caret_moved) + changed->selection_changed (changed->data, "text-caret-moved", changed->cursor_position); + + if (caret_moved || bound_moved) + changed->selection_changed (changed->data, "text-selection-changed", 0); +} + +static void +notify_cb (GObject *object, + GParamSpec *pspec, + TextChanged *changed) +{ + if (g_strcmp0 (pspec->name, "cursor-position") == 0 || + g_strcmp0 (pspec->name, "selection-bound") == 0) + { + int cursor_position, selection_bound; + + gtk_editable_get_selection_bounds (GTK_EDITABLE (object), &cursor_position, &selection_bound); + update_selection (changed, cursor_position, selection_bound); + } +} + +static void +update_cursor (GtkTextBuffer *buffer, + TextChanged *changed) +{ + GtkTextIter iter; + int cursor_position, selection_bound; + + gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_insert (buffer)); + cursor_position = gtk_text_iter_get_offset (&iter); + + gtk_text_buffer_get_iter_at_mark (buffer, &iter, gtk_text_buffer_get_selection_bound (buffer)); + + selection_bound = gtk_text_iter_get_offset (&iter); + + update_selection (changed, cursor_position, selection_bound); +} + +static void +insert_range_cb (GtkTextBuffer *buffer, + GtkTextIter *iter, + char *text, + int len, + TextChanged *changed) +{ + int position; + int length; + + position = gtk_text_iter_get_offset (iter); + length = g_utf8_strlen (text, len); + + changed->text_changed (changed->data, "insert", position - length, length, text); + + update_cursor (buffer, changed); +} + +static void +delete_range_cb (GtkTextBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end, + TextChanged *changed) +{ + int offset, length; + char *text; + + text = gtk_text_buffer_get_slice (buffer, start, end, FALSE); + + offset = gtk_text_iter_get_offset (start); + length = gtk_text_iter_get_offset (end) - offset; + + changed->text_changed (changed->data, "delete", offset, length, text); + + g_free (text); +} + +static void +delete_range_after_cb (GtkTextBuffer *buffer, + GtkTextIter *start, + GtkTextIter *end, + TextChanged *changed) +{ + update_cursor (buffer, changed); +} + +static void +mark_set_cb (GtkTextBuffer *buffer, + GtkTextIter *location, + GtkTextMark *mark, + TextChanged *changed) +{ + if (mark == gtk_text_buffer_get_insert (buffer) || + mark == gtk_text_buffer_get_selection_bound (buffer)) + update_cursor (buffer, changed); +} + +static void +buffer_changed (GtkWidget *widget, + GParamSpec *pspec, + TextChanged *changed) +{ + GtkTextBuffer *buffer; + GtkTextIter start, end; + char *text; + + buffer = gtk_text_view_get_buffer (GTK_TEXT_VIEW (widget)); + + if (changed->buffer) + { + g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed); + + gtk_text_buffer_get_bounds (changed->buffer, &start, &end); + text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE); + changed->text_changed (changed->data, "delete", 0, gtk_text_buffer_get_char_count (changed->buffer), text); + g_free (text); + + update_selection (changed, 0, 0); + + g_clear_object (&changed->buffer); + } + + changed->buffer = buffer; + + if (changed->buffer) + { + g_object_ref (changed->buffer); + g_signal_connect (changed->buffer, "insert-text", G_CALLBACK (insert_range_cb), changed); + g_signal_connect (changed->buffer, "delete-range", G_CALLBACK (delete_range_cb), changed); + g_signal_connect_after (changed->buffer, "delete-range", G_CALLBACK (delete_range_after_cb), changed); + g_signal_connect_after (changed->buffer, "mark-set", G_CALLBACK (mark_set_cb), changed); + + gtk_text_buffer_get_bounds (changed->buffer, &start, &end); + text = gtk_text_buffer_get_slice (changed->buffer, &start, &end, FALSE); + changed->text_changed (changed->data, "insert", 0, gtk_text_buffer_get_char_count (changed->buffer), text); + g_free (text); + + update_cursor (changed->buffer, changed); + } +} + +void +gtk_atspi_connect_text_signals (GtkWidget *widget, + GtkAtspiTextChangedCallback text_changed, + GtkAtspiSelectionChangedCallback selection_changed, + gpointer data) +{ + TextChanged *changed; + + changed = g_new0 (TextChanged, 1); + changed->text_changed = text_changed; + changed->selection_changed = selection_changed; + changed->data = data; + + g_object_set_data_full (G_OBJECT (widget), "accessible-text-data", changed, g_free); + + if (GTK_IS_EDITABLE (widget)) + { + GtkText *text = gtk_editable_get_text_widget (widget); + + if (text) + { + g_signal_connect_after (text, "insert-text", G_CALLBACK (insert_text_cb), changed); + g_signal_connect (text, "delete-text", G_CALLBACK (delete_text_cb), changed); + g_signal_connect (text, "notify", G_CALLBACK (notify_cb), changed); + + gtk_editable_get_selection_bounds (GTK_EDITABLE (text), &changed->cursor_position, &changed->selection_bound); + } + } + else if (GTK_IS_TEXT_VIEW (widget)) + { + g_signal_connect (widget, "notify::buffer", G_CALLBACK (buffer_changed), changed); + buffer_changed (widget, NULL, changed); + } +} + +void +gtk_atspi_disconnect_text_signals (GtkWidget *widget) +{ + TextChanged *changed; + + changed = g_object_get_data (G_OBJECT (widget), "accessible-text-data"); + + g_assert (changed != NULL); + + if (GTK_IS_EDITABLE (widget)) + { + GtkText *text = gtk_editable_get_text_widget (widget); + + if (text) + { + g_signal_handlers_disconnect_by_func (text, insert_text_cb, changed); + g_signal_handlers_disconnect_by_func (text, delete_text_cb, changed); + g_signal_handlers_disconnect_by_func (text, notify_cb, changed); + } + } + else if (GTK_IS_TEXT_VIEW (widget)) + { + g_signal_handlers_disconnect_by_func (widget, buffer_changed, changed); + if (changed->buffer) + { + g_signal_handlers_disconnect_by_func (changed->buffer, insert_range_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, delete_range_after_cb, changed); + g_signal_handlers_disconnect_by_func (changed->buffer, mark_set_cb, changed); + } + g_clear_object (&changed->buffer); + } + + g_object_set_data (G_OBJECT (widget), "accessible-text-data", NULL); +} diff --git a/gtk/a11y/gtkatspitextprivate.h b/gtk/a11y/gtkatspitextprivate.h index 2ee9b2d85f..14ebf8c2b4 100644 --- a/gtk/a11y/gtkatspitextprivate.h +++ b/gtk/a11y/gtkatspitextprivate.h @@ -27,4 +27,19 @@ G_BEGIN_DECLS const GDBusInterfaceVTable *gtk_atspi_get_text_vtable (GtkWidget *widget); +typedef void (GtkAtspiTextChangedCallback) (gpointer data, + const char *kind, + int start, + int end, + const char *text); +typedef void (GtkAtspiSelectionChangedCallback) (gpointer data, + const char *kind, + int position); + +void gtk_atspi_connect_text_signals (GtkWidget *widget, + GtkAtspiTextChangedCallback text_changed, + GtkAtspiSelectionChangedCallback selection_changed, + gpointer data); +void gtk_atspi_disconnect_text_signals (GtkWidget *widget); + G_END_DECLS -- 2.30.2